package msg;

import java.sql.*;
import java.util.*;
import java.util.Date;

import user.*;

import common.Pair;
import common.dal.*;
import common.dal.SQLDataColumn.Constraints;
import common.exceptions.*;
import common.extensions.*;
import common.interfaces.IMessagePersistencyManager;

/**
 * A database implementation of {@link IMessagePersistencyManager}
 * 
 * @author Gal Lalouche
 */
public class DBMessagePersistencyManager extends AbstractDAL<Message, Long> implements IMessagePersistencyManager {
	/**
	 * Creates a new DBMessagePersistencyManager
	 * 
	 * @param t The transaction all queries from this DB will be under
	 */
	public DBMessagePersistencyManager(Transaction t) {
		super(t);
	}
	
	/**
	 * The table's name
	 */
	public final static String	TABLE_NAME			= "messages";
	/**
	 * The table's primary key
	 */
	public final static String	PRIMARY_KEY			= "id";
	
	private final static String	TIME				= "time";
	private final static String	CAPACITY			= "capacity";
	private final static String	POSTER				= "poster";
	private final static String	CONTENT				= "content";
	private final static String	LOCATION_LONG		= "locationLong";
	private final static String	LOCATION_LAT		= "locationLat";
	private final static String	LOCATION_ADDRESS	= "locationAddress";
	private static final String	IS_EVENT			= "isEvent";
	
	@Override
	protected SQLDataColumn[] getTableColumns() {
		return new SQLDataColumn[] {
				new SQLDataColumn(PRIMARY_KEY, SQLDataType.BIGINT, Constraints.PRIMARY_KEY),
				new SQLDataColumn(LOCATION_LAT, SQLDataType.DOUBLE,
						EnumSet.of(Constraints.NOT_NULL, Constraints.INDEX)),
				new SQLDataColumn(LOCATION_LONG, SQLDataType.DOUBLE,
						EnumSet.of(Constraints.NOT_NULL, Constraints.INDEX)),
				new SQLDataColumn(LOCATION_ADDRESS, SQLDataType.TINYTEXT),
				new SQLDataColumn(CONTENT, SQLDataType.TEXT, Constraints.NOT_NULL),
				new SQLDataColumn(POSTER, SQLDataType.VARCHAR_50,
						EnumSet.of(Constraints.NOT_NULL, Constraints.FOREIGN_KEY, Constraints.INDEX),
						Pair.of(DBPasswordManager.TABLE_NAME, DBPasswordManager.PRIMARY_KEY)),
				new SQLDataColumn(TIME, SQLDataType.BIGINT, Constraints.NOT_NULL),
				new SQLDataColumn(IS_EVENT, SQLDataType.BOOLEAN,
						EnumSet.of(Constraints.INDEX, Constraints.NOT_NULL)),
				new SQLDataColumn(CAPACITY, SQLDataType.INTEGER)};
	}
	
	@Override
	protected String getTableName() {
		return TABLE_NAME;
	}
	
	@Override
	protected String getPrimaryColumnName() {
		return PRIMARY_KEY;
	}
	
	private Message generateFromResultSetStatic(ResultSet result) throws SQLException {
		User user;
		try {
			user = LoginManager.getNewInstance(t).get(result.getString(POSTER));
		} catch (InvalidKeyException e) {
			throw new AssertionError("User has been deleted from the system without his messages being removed", e);
		}
		return generateResultFromSet(result, user);
	}
	
	@Override
	protected Message generateFromResultSet(ResultSet result) throws SQLException {
		return generateFromResultSetStatic(result);
	}
	
	// generates the result from the user with user
	private static Message generateResultFromSet(ResultSet result, User user) throws SQLException {
		Location loc = new Location(result.getDouble(LOCATION_LAT), result.getDouble(LOCATION_LONG),
				result.getString(LOCATION_ADDRESS));
		if (result.getObject(CAPACITY) == null) {
			return new Message(result.getLong(PRIMARY_KEY), loc, result.getString(CONTENT), user,
					result.getLong(TIME));
		} else {
			return new EventMessage(result.getLong(PRIMARY_KEY), loc, result.getString(CONTENT), user,
					result.getLong(TIME), result.getInt(CAPACITY));
		}
	}
	
	// is used when there is no user to cache
	private Collection<? extends Message> getAllMessagesFromSet(ResultSet set) throws SQLException {
		Collection<Message> result = new LinkedList<>();
		while (set.next()) {
			result.add(generateFromResultSetStatic(set));
		}
		
		return result;
	}
	
	// is used to cache the user instead of polling him constantly
	private static Collection<? extends Message> getAllMessagesFromSet(ResultSet set, User user)
			throws SQLException {
		Collection<Message> result = new LinkedList<>();
		while (set.next()) {
			result.add(generateResultFromSet(set, user));
		}
		
		return result;
	}
	
	private static WhereEntry getMessageEntry() {
		return new WhereEntry(IS_EVENT, WhereOperator.EQUALS, false);
	}
	
	private static WhereEntry getEventEntry() {
		return new WhereEntry(IS_EVENT, WhereOperator.EQUALS, true);
	}
	
	// adds a since entry if needed
	private static void addSinceEntry(Long since, Collection<WhereEntry> entries) {
		if (since != null) {
			entries.add(new WhereEntry(TIME, WhereOperator.GREATER_THAN, since));
		}
	}
	
	// gets the time to use, w.r.t. since and includePastEvents
	private static Long getTimePast(Long since, boolean includePastEvents) {
		Long actualSince = since;
		if (includePastEvents == false) {
			if (actualSince == null) {
				actualSince = new Date().getTime();
			} else {
				actualSince = Math.max(actualSince, new Date().getTime());
			}
		}
		return actualSince;
	}
	
	@Override
	public void store(Message message) throws SQLException, InvalidUserException {
		try {
			message.accept(this);
		} catch (InvalidUserException | ExistingEntityException | SQLException e) {
			throw e;
		} catch (Exception e) {
			throw new AssertionError(e);
		}
	}
	
	/**
	 * Checks that the given user parameters exists in the system
	 * 
	 * @param user The user to check
	 * @throws InvalidUserException If the user does not exist in the system
	 * @throws SQLException
	 */
	private User checkUserParameter(User user) throws SQLException, InvalidUserException {
		if (user == null) {
			throw new IllegalArgumentException("user is null");
		}
		try {
			return LoginManager.getNewInstance(t).get(user.getLogin());
		} catch (InvalidKeyException e) {
			throw new InvalidUserException(user);
		}
	}
	
	@SuppressWarnings("unchecked")
	@Override
	public Collection<Message> getAllUserPosts(User userIn) throws SQLException, InvalidUserException {
		User user = checkUserParameter(userIn);
		try (ResultSet set = selectWhere(new WhereEntry(POSTER, WhereOperator.EQUALS, user.getLogin()))) {
			return (Collection<Message>)getAllMessagesFromSet(set, user);
		}
	}
	
	@SuppressWarnings("unchecked")
	@Override
	public Collection<EventMessage> getAllUserEvents(User userIn, Long since, boolean includePastEvents)
			throws InvalidUserException, SQLException {
		User user = checkUserParameter(userIn);
		Long actualSince = getTimePast(since, includePastEvents);
		Collection<WhereEntry> entries = new LinkedList<>(Arrays.asList(new WhereEntry(POSTER,
				WhereOperator.EQUALS, user.getLogin()), getEventEntry()));
		addSinceEntry(actualSince, entries);
		// it is certain that it is > 0, if it exists at all
		try (ResultSet set = selectWhere(entries)) {
			return (Collection<EventMessage>)getAllMessagesFromSet(set, user);
		}
	}
	
	@SuppressWarnings("unchecked")
	@Override
	public Collection<Message> getAllUserMessagesOnly(User userIn, Long since) throws InvalidUserException,
			SQLException {
		User user = checkUserParameter(userIn);
		// it is certain that it is > 0, if it exists at all
		Collection<WhereEntry> entries = new LinkedList<>(Arrays.asList(new WhereEntry(POSTER,
				WhereOperator.EQUALS, user.getLogin()), getMessageEntry()));
		addSinceEntry(since, entries);
		try (ResultSet set = selectWhere(entries)) {
			return (Collection<Message>)getAllMessagesFromSet(set, user);
		}
	}
	
	@Override
	public void removeAllUserMessagesAndEvents(User user) throws SQLException, InvalidUserException {
		checkUserParameter(user);
		deleteWhere(new WhereEntry(POSTER, WhereOperator.EQUALS, user.getLogin()));
	}
	
	@Override
	public long getLastId() throws SQLException {
		try (ResultSet set = max(getPrimaryColumnName())) {
			boolean result = set.first();
			assert result;
			return set.getLong(1);
		}
	}
	
	private static Collection<Object> getMessageInsertionParamters(Message message) {
		return new LinkedList<>(Arrays.asList(message.getId(), message.getLocation().getLatitude(), message
				.getLocation().getLongitude(), message.getLocation().getAddress(), message.getContent(), message
				.getPoster().getId(), message.getTime(), message instanceof EventMessage));
	}
	
	private static Collection<Object> getEventInsertionParamters(EventMessage event) {
		Collection<Object> defaultParams = getMessageInsertionParamters(event);
		defaultParams.add(event.getCapacity());
		return defaultParams;
	}
	
	@Override
	public Message visit(Message message) throws Exception {
		if (message == null) {
			throw new IllegalArgumentException("message is null");
		}
		if (exists(message.getId())) {
			throw new ExistingEntityException(message);
		}
		try {
			insertInto(getMessageInsertionParamters(message).toArray());
		} catch (SQLIntegrityConstraintViolationException e) {
			throw new InvalidUserException(message.getPoster());
		}
		return message;
	}
	
	@Override
	public Message visit(EventMessage event) throws Exception {
		if (event == null) {
			throw new IllegalArgumentException("message is null");
		}
		if (exists(event.getId())) {
			throw new ExistingEntityException(event);
		}
		try {
			insertInto(getEventInsertionParamters(event).toArray());
		} catch (SQLIntegrityConstraintViolationException e) {
			throw new InvalidUserException(event.getPoster());
		}
		return event;
	}
	
	// not to break testing
	Collection<Message> getAllMessagesInRadius(Location loc, final double distance) throws SQLException {
		return getAllMessagesInRadius(loc, distance, null);
	}
	
	@SuppressWarnings("unchecked")
	@Override
	public Collection<EventMessage> getAllEventsInRadius(Location loc, final double distance, Long since,
			boolean includePastEvents) throws SQLException {
		Long actualSince = getTimePast(since, includePastEvents);
		return (Collection<EventMessage>)getInRadiusPrivate(loc, distance, actualSince, true);
	}
	
	@SuppressWarnings("unchecked")
	@Override
	public Collection<Message> getAllMessagesInRadius(Location loc, final double distance, Long since)
			throws SQLException {
		return (Collection<Message>)getInRadiusPrivate(loc, distance, since, false);
	}
	
	// events = null -> all posts, events = true -> only events, events = false -> only messages
	private Collection<? extends Message> getInRadiusPrivate(final Location loc, final double distance,
			Long since, Boolean events) throws SQLException {
		// we first calculate using Manhattan Distance for efficiency / indexing sakes,
		// then we only choose those messages that are within the circle (Euclidean distance)
		// we regard the earth as flat (what do you mean it isn't?)
		Collection<WhereEntry> entries = new LinkedList<>();
		Pair<Location, Location> bounds = loc.getBoundingCoordinates(distance);
		entries.add(new WhereEntry(LOCATION_LAT, WhereOperator.GREATER_THAN, bounds.first.getLatitude()));
		entries.add(new WhereEntry(LOCATION_LONG, WhereOperator.GREATER_THAN, bounds.first.getLongitude()));
		entries.add(new WhereEntry(LOCATION_LAT, WhereOperator.LESS_THAN, bounds.second.getLatitude()));
		entries.add(new WhereEntry(LOCATION_LONG, WhereOperator.LESS_THAN, bounds.second.getLongitude()));
		
		if (events != null) {
			if (events) {
				entries.add(getEventEntry());
			} else {
				entries.add(getMessageEntry());
			}
		}
		addSinceEntry(since, entries);
		
		try (ResultSet set = selectWhere(entries)) {
			@SuppressWarnings("unchecked")
			Collection<Message> manhattanMsgs = (Collection<Message>)getAllMessagesFromSet(set);
			return Extender.where(manhattanMsgs, new Predicate<Message>() {
				private double getEuclideanDistance(Message element) {
					return element.getLocation().getDistanceTo(loc);
				}
				
				@Override
				public Boolean calculate(Message element) {
					// there is about 0.1 meters error due to precision rounding
					return getEuclideanDistance(element) <= (distance + 1);
				}
			});
		}
	}
}
